package jass.render;
import javax.sound.sampled.*;

/**
   Utility class to read audio in real-time. Use appropriate constructor to use JavaSound or native sound support
   @author Kees van den Doel (kvdoel@cs.ubc.ca)
*/
public class RTAudioIn extends Thread {
    private float srate;
    private int bitsPerFrame;
    private int nchannels;
    private boolean signed;
    private static final boolean bigEndian = false;
    private AudioFormat audioFormat; 
    private TargetDataLine targetDataLine;
    private int scratchBufsz; // byte buffer size
    private byte[] bbuf;
    private int bufferSize; // JavaSound buffersize
    private String preferredMixer = null;
    private Mixer mixer;

    // for native sound
    private boolean useNative = false; // can use native audio
    private long nativeObjectPointer = 0;

    /** Initialize native sound for RtAudio
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param nchannels number of audio channels.
        @param srate sampling rate in Hertz.
        @param bufferSizeJass jass buffersize
        @param numRtAudioBuffers affectiing latency, 0 gives best latency
        @return long representing C++ object pointer
    */
    public native long initNativeSound(int nchannels,int srate,int bufferSizeJass,int numRtAudioBuffers);

    /** Read a buffer of floats using native sound.
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param nativeObjectPointer representing C++ object pointer.
        @param buf array of float with sound buffer.
        @param buflen length of buffer.
    */
    public native void readNativeSoundFloat(long nativeObjectPointer,float[] buf,int buflen);


    /** Close native sound
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param nativeObjectPointer representing C++ object pointer
    */
    public native void closeNativeSound(long nativeObjectPointer);

    private void findMixer() {
        Mixer.Info[] mixerinfo = AudioSystem.getMixerInfo();
        int mixerIndex = 0;
        if(preferredMixer != null) {
            for(int i=0;i<mixerinfo.length;i++) {
                // list the mixers
                System.out.println("mixer "+i+": ");
                //System.out.println("description:"+mixerinfo[i].getDescription());
                String name = mixerinfo[i].getName();
                System.out.println("name:"+name);
                if(name.equalsIgnoreCase(preferredMixer)) {
                    mixerIndex = i;
                }
                //System.out.println("vendor:"+mixerinfo[i].getVendor());
                //System.out.println("version:"+mixerinfo[i].getVersion());
                //System.out.println("string:"+mixerinfo[i].toString());
            }
        }
    
        System.out.println("CHOSEN INPUT MIXER: "+mixerinfo[mixerIndex].getName());
        mixer = AudioSystem.getMixer(mixerinfo[mixerIndex]);
    }

    /* num_fragments must equal bufferSize of JASS patch whern using RtAudio on LINUX, it is what its name
       indicates when using DirectSound
     */
    private void initAudioNative(float srate,int nchannels,int num_fragments) {
        int numRtAudioBuffers = 0;
        initAudioNative(srate,nchannels,num_fragments,numRtAudioBuffers);
    }

    private void initAudioNative(float srate,int nchannels,int buffersizeJass,int numRtAudioBuffers) {
        this.srate = srate;
        this.nchannels = nchannels;
        nativeObjectPointer = initNativeSound(nchannels,(int)srate,buffersizeJass,numRtAudioBuffers);
        
        //This will ensure that close() gets called before the program exits
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                System.out.println("The shutdown hook in RTAudioIn is executing");
                close();
                try{	//wait for the DLL to do its destruction before terminating!
                    sleep(100);
                } catch(Exception e) {
                    System.out.print("The sleep function is broken");
                }
            }
	    });
        
    }
    
    private void initAudio(int bufferSize, float srate,int bitsPerFrame,int nchannels,boolean signed) {
        this.srate = srate;
        this.bufferSize = bufferSize;
        this.bitsPerFrame = bitsPerFrame;
        this.nchannels = nchannels;
        this.signed = signed;
        findMixer();
        audioFormat = new AudioFormat(srate,bitsPerFrame,nchannels,signed,bigEndian);
        DataLine.Info info;
        if(bufferSize == 0) {
            info = new DataLine.Info(TargetDataLine.class, audioFormat);
        } else {
            info = new DataLine.Info(TargetDataLine.class, audioFormat,bufferSize);
        }
        if (!AudioSystem.isLineSupported(info)) {
            System.out.println(getClass().getName()+" : Error gettting targetDataLine: not supported\n");
        }
        // BUG
        if (!mixer.isLineSupported(info)) {
            // this may be a bogus message, e.g. on Tritonus this is not reliable information
            System.out.println(getClass().getName()
                               +" : Error: sourcedataline: not supported (this may be a bogus message under Tritonus)\n");
        }
        try {
            // <BUG>
            // For some reason can't get a line from the mixer, but I can get it from AudioSystem
            //
            //targetDataLine = (TargetDataLine) mixer.getLine(info); // should do this but fails??
            targetDataLine = (TargetDataLine) AudioSystem.getLine(info); // workaround, ignore preferred mixer
            // </BUG>
            if(bufferSize == 0) {
                targetDataLine.open(audioFormat);
            } else {
                targetDataLine.open(audioFormat,bufferSize);
            }
            targetDataLine.start();
        } catch (LineUnavailableException ex) {
            System.out.println(getClass().getName()+" : Error getting line, trying again in 1 sec\n");
            // sometimes it works the second time
            try {
                sleep(1000);
            } catch(Exception e3) {
            }
            try {
                targetDataLine = (TargetDataLine) mixer.getLine(info);
                if(bufferSize == 0) {
                    targetDataLine.open(audioFormat);
                } else {
                    targetDataLine.open(audioFormat,bufferSize);
                }
                targetDataLine.start();
            } catch (LineUnavailableException ex2) {
                System.out.println(getClass().getName()+" : Error getting line 2nd time, giving up\n");
            }
        }
        scratchBufsz = 4;
        bbuf = new byte[scratchBufsz];
    }

    /** Constructor. Uses JavaSound with default mixer
        @param bufferSize Buffer size used by JavaSound.
        @param srate sampling rate in Hertz.
        @param bitsPerFrame number of bits per audio frame.
        @param nchannels number of audio channels.
        @param signed true if signed format, false otherwise.
    */
    public RTAudioIn(int buffersize, float srate,int bitsPerFrame,int nchannels,boolean signed) {
        initAudio(buffersize,srate,bitsPerFrame,nchannels,signed);
    }

    /** Constructor. Uses JavaSound and can set mixer to use (e.g. "Esd Mixer")
        @param bufferSize Buffer size used by JavaSound.
        @param srate sampling rate in Hertz.
        @param bitsPerFrame number of bits per audio frame.
        @param nchannels number of audio channels.
        @param signed true if signed format, false otherwise.
        @param prefMixer preferred mixer as name string
    */
    public RTAudioIn(int buffersize, float srate,int bitsPerFrame,int nchannels,boolean signed,String prefMixer) {
        this.preferredMixer = prefMixer;
        initAudio(buffersize,srate,bitsPerFrame,nchannels,signed);
    }

    /** Constructor. Uses default high latency JavaSound buffersize and default mixer
        @param srate sampling rate in Hertz.
        @param bitsPerFrame number of bits per audio frame.
        @param nchannels number of audio channels.
        @param signed true if signed format, false otherwise.
    */
    public RTAudioIn(float srate,int bitsPerFrame,int nchannels,boolean signed) {
        initAudio(0,srate,bitsPerFrame,nchannels,signed);
    }
    
    /** Constructor. Uses native sound capture.
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param srate sampling rate in Hertz.
        @param nchannels number of audio channels.
        @param bufferSizeJass jass bufsz
        @param numRtAudioBuffers rtaudio parameter related to latency. 0 lowest
    */
    public RTAudioIn(float srate,int nchannels,int bufferSizeJass,int numRtAudioBuffers) {
        useNative = true;
        // load shared library with native sound implementations
        try {
            System.loadLibrary("rtaudio");
        } catch(UnsatisfiedLinkError e) {
            System.out.println("Could not load shared library rtaudio: "+e);
        }
        initAudioNative(srate,nchannels,bufferSizeJass,numRtAudioBuffers);
    }
    
    /** Constructor. Uses native sound capture. Uses default (0) for numRtAudioBuffers
        This is a native method and needs librtaudio.so (LINUX) or rtaudio.dll (Windows)
        @param srate sampling rate in Hertz.
        @param nchannels number of audio channels.
        @param bufferSizeJass jass bufsz
    */
    public RTAudioIn(float srate,int nchannels,int bufferSizeJass) {
        useNative = true;
        int numRtAudioBuffers = 0; // use lowest possible
        // load shared library with native sound implementations
        try {
            System.loadLibrary("rtaudio");
        } catch(UnsatisfiedLinkError e) {
                System.out.println("Could not load shared library rtaudio: "+e);
        }
        initAudioNative(srate,nchannels,bufferSizeJass,numRtAudioBuffers);
    }

    /** Read audio buffer from input queue and block if queue is empty.
        @param y buffer to write to.
        @param nsamples number of samples required.
    */
    public void read(float [] y,int nsamples) {
        if(useNative) {
            readNativeSoundFloat(nativeObjectPointer, y, nsamples);
        } else {
            if(2*nsamples > scratchBufsz) {
                scratchBufsz = 2 * nsamples;
                bbuf = new byte[scratchBufsz];
            }
            targetDataLine.read(bbuf,0,2*nsamples);
            FormatUtils.byteToFloat(y,bbuf,nsamples);
        }
    }

    //** Close resources */
    public void close() {
        if(useNative) {
	    if(nativeObjectPointer != 0) {
		closeNativeSound(nativeObjectPointer);      
		nativeObjectPointer = 0;      
	    }
        } else {
            targetDataLine.stop();
            targetDataLine.close();
        }
    }
    
}
